package com.freewater.library.lending.book.model;

import com.freewater.library.common.domain.BaseAggregateRoot;
import com.freewater.library.common.domain.DomainException;
import com.freewater.library.common.domain.Nullable;
import com.freewater.library.lending.book.model.event.*;
import lombok.Getter;
import org.apache.commons.lang3.Validate;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 一本书
 */
@Getter
public class BookInstance extends BaseAggregateRoot {
    private static final int MAX_RESERVE_OVERDUE_HOURS = 24;
    private static final int MAX_CHECKED_OUT_OVERDUE_DAYS = 30;
    /**
     * 二维码
     */
    private final String qrCode;
    /**
     * ISBN
     */
    private final String isbn;
    /**
     * 是否已下架
     */
    private boolean takenOff;
    /**
     * 是否已预定
     */
    private boolean reserved;
    /**
     * 是否已借出
     */
    private boolean checkedOut;
    /**
     * 是否已遗失
     */
    private boolean lost;
    /**
     * 预定读者ID
     */
    private Long reservedBy;
    /**
     * 预定时间
     */
    private LocalDateTime reservedAt;
    /**
     * 借出读者ID
     */
    private Long checkedOutBy;
    /**
     * 借出时间
     */
    private LocalDateTime checkedOutAt;
    /**
     * 借出是否已超时
     */
    private Boolean checkedOutOverdueMarked;

    @Override
    public Serializable getId() {
        return this.qrCode;
    }

    public BookInstance(String qrCode, String isbn) {
        Validate.notBlank(qrCode, "二维码不能为空");
        Validate.notNull(isbn, "ISBN不能为空");
        this.qrCode = qrCode;
        this.isbn = isbn;
        this.takenOff = false;
        this.reserved = false;
        this.checkedOut = false;
        this.lost = false;
        this.reservedBy = null;
        this.reservedAt = null;
        this.checkedOutBy = null;
        this.checkedOutAt = null;
        this.checkedOutOverdueMarked = null;
    }

    // 给JPA使用
    public BookInstance(String qrCode,
                        String isbn,
                        boolean takenOff,
                        boolean reserved,
                        boolean checkedOut,
                        boolean lost,
                        @Nullable Long reservedBy,
                        @Nullable LocalDateTime reservedAt,
                        @Nullable Long checkedOutBy,
                        @Nullable LocalDateTime checkedOutAt,
                        @Nullable Boolean checkedOutOverdueMarked) {
        Validate.notBlank(qrCode, "二维码不能为空");
        Validate.notNull(isbn, "ISBN不能为空");
        this.qrCode = qrCode;
        this.isbn = isbn;
        this.takenOff = takenOff;
        this.reserved = reserved;
        this.checkedOut = checkedOut;
        this.lost = lost;
        this.reservedBy = reservedBy;
        this.reservedAt = reservedAt;
        this.checkedOutBy = checkedOutBy;
        this.checkedOutAt = checkedOutAt;
        this.checkedOutOverdueMarked = checkedOutOverdueMarked;
    }

    /**
     * 上架：如果已遗失则会被修改为未遗失
     */
    public void putOn() {
        if (!takenOff) {
            return;
        }
        this.takenOff = false;
        if (this.lost) {
            this.lost = false;
        }
        this.raiseEvent(new BookInstancePuttedOnEvent(this.qrCode, this.isbn));
    }

    /**
     * 下架：会取消可能存在的预定
     */
    public void takeOff() {
        if (takenOff) {
            return;
        }
        // 如果有人预定了此书，则取消预定
        if (this.isReserving()) {
            this.cancelReserve("书本下架");
        }
        this.takenOff = true;
        this.raiseEvent(new BookInstanceTakenOffEvent(this.qrCode, this.isbn));
    }

    /**
     * 预定
     *
     * @param readerId 读者Id
     * @param time     预定时间，默认为当前系统时间
     */
    public void reserve(Long readerId, @Nullable LocalDateTime time) {
        Validate.notNull(readerId, "读者Id不能为空");
        if (!this.canBeReserved()) {
            if (this.isReservingBy(readerId)) { // 如果已被此读者预定则直接返回
                return;
            }
            throw new DomainException("此书当前不能被预定");
        }
        this.reserved = true;
        this.reservedBy = readerId;
        this.reservedAt = time == null ? LocalDateTime.now() : time;
        this.raiseEvent(new BookInstanceReservedEvent(this.qrCode, this.isbn, readerId, this.reservedAt));
    }

    /**
     * 取消预定
     */
    public void cancelReserve(String reason) {
        Validate.notBlank(reason, "取消预定原因不能为空");
        if (!this.isReserving()) {
            return;
        }
        BookInstanceReserveCanceledEvent canceledEvent =
                new BookInstanceReserveCanceledEvent(this.qrCode, this.isbn, this.reservedBy, this.reservedAt, reason);
        this.reserved = false;
        this.reservedBy = null;
        this.reservedAt = null;
        this.raiseEvent(canceledEvent);
    }

    /**
     * 计算是否预定超时：预定超过24小时则超时
     *
     * @param nowTime 计算超时的参考时间点，默认为当期系统时间
     * @return 是否预定超时
     */
    public boolean isReservedOverdue(@Nullable LocalDateTime nowTime) {
        nowTime = nowTime == null ? LocalDateTime.now() : nowTime;
        if (!this.isReserving()) {
            throw new DomainException("当前未被预定，无法计算是否预定超时");
        }
        return this.reservedAt.isBefore(nowTime.minusHours(MAX_RESERVE_OVERDUE_HOURS));
    }

    /**
     * 预定超时：预定后24小时超时，超时后自定取消预定
     *
     * @param nowTime 计算超时参考时间点，默认为当前系统时间
     */
    public void reservedOverdue(@Nullable LocalDateTime nowTime) {
        if (!this.isReserving()) {
            return;
        }
        nowTime = nowTime == null ? LocalDateTime.now() : nowTime;
        if (!isReservedOverdue(nowTime)) {
            return;
        }
        BookInstanceReserveOverdueEvent overdueEvent =
                new BookInstanceReserveOverdueEvent(this.qrCode, this.isbn, this.reservedBy);
        this.raiseEvent(overdueEvent);
        // 取消预定
        this.cancelReserve("预定已超时");
    }

    /**
     * 借出：如果是本人预定的需要清除预定信息
     *
     * @param readerId 读者Id
     * @param time     借出时间，默认为当前系统时间
     */
    public void checkOut(Long readerId, @Nullable LocalDateTime time) {
        Validate.notNull(readerId, "借阅人Id不能为空");
        boolean canCheckOut = this.isCanCheckOut(readerId);
        if (!canCheckOut) {
            throw new DomainException("此书当前不能被借出");
        }
        time = time == null ? LocalDateTime.now() : time;
        BookInstanceCheckedOutEvent checkedOutEvent =
                new BookInstanceCheckedOutEvent(this.qrCode, this.isbn, readerId, this.reservedAt, time);
        // 清空预定信息
        if (this.reserved) {
            this.reserved = false;
            this.reservedBy = null;
            this.reservedAt = null;
        }
        // 借出
        this.checkedOut = true;
        this.checkedOutBy = readerId;
        this.checkedOutAt = time;
        this.checkedOutOverdueMarked = false;
        this.raiseEvent(checkedOutEvent);
    }

    /**
     * 计算借出是否已超时：借出后30天超时
     *
     * @param nowTime 计算超时参考时间点，默认为当前系统时间
     * @return 借出是否已超时
     */
    public boolean isCheckedOutOverdue(@Nullable LocalDateTime nowTime) {
        nowTime = nowTime == null ? LocalDateTime.now() : nowTime;
        if (!this.isCheckingOut()) {
            throw new DomainException("当前没有被借出，无法计算是否借出超时");
        }
        return this.checkedOutAt.isBefore(nowTime.minusDays(MAX_CHECKED_OUT_OVERDUE_DAYS));
    }

    /**
     * 借出已超时
     *
     * @param nowTime 计算超时参考时间点，默认为当前系统时间
     */
    public void checkedOutOverdue(@Nullable LocalDateTime nowTime) {
        if (!this.isCheckingOut()) {
            return;
        }
        if (!this.isCheckedOutOverdue(nowTime)) {
            return;
        }
        if (this.isCheckedOutOverdueMarked()) {
            return;
        }
        BookInstanceCheckedOutOverdueEvent checkOutOverdueEvent =
                new BookInstanceCheckedOutOverdueEvent(this.qrCode, this.isbn, this.checkedOutBy, this.checkedOutAt);
        this.checkedOutOverdueMarked = true;
        this.raiseEvent(checkOutOverdueEvent);
    }

    /**
     * 归还
     */
    public void returnBack() {
        if (!this.isCheckingOut()) {
            return;
        }
        BookInstanceReturnedEvent returnedEvent =
                new BookInstanceReturnedEvent(this.qrCode, this.isbn, this.checkedOutBy, this.checkedOutAt);
        this.checkedOut = false;
        this.checkedOutBy = null;
        this.checkedOutAt = null;
        this.checkedOutOverdueMarked = null;
        this.raiseEvent(returnedEvent);
        // 上架遗失的书
        if (this.lost) {
            this.putOn();
        }
    }

    /**
     * 借出后上报遗失
     */
    public void submitLost() {
        if (lost) {
            return;
        }
        if (!this.isCheckingOut()) {
            return;
        }
        this.lost = true;
        this.raiseEvent(new BookInstanceLostEvent(this.qrCode, this.isbn, this.checkedOutBy, this.checkedOutAt));
        // 下架遗失的书
        if (!this.takenOff) {
            this.takeOff();
        }
    }

    /**
     * 是否可预定
     *
     * @return 是否可预定
     */
    private boolean canBeReserved() {
        return !takenOff && !reserved && !checkedOut && !lost;
    }

    /**
     * 是否被人预定中
     *
     * @return 是否被人预定中
     */
    public boolean isReserving() {
        return !takenOff && !checkedOut && !lost && reserved;
    }

    /**
     * 是否被指定的读者预定中
     *
     * @param readerId 读者Id
     * @return 是否被指定的读者预定中
     */
    public boolean isReservingBy(Long readerId) {
        return this.isReserving() && readerId.equals(this.reservedBy);
    }

    /**
     * 书是否可以被此读者借出：可以被任何人预定或者被当前借书人约定，则可以借阅此书
     *
     * @param readerId 读者Id
     * @return 是否可以被此读者借出
     */
    private boolean isCanCheckOut(Long readerId) {
        return this.canBeReserved() || this.isReservingBy(readerId);
    }

    /**
     * 是否被借出中
     *
     * @return 是否被借出中
     */
    private boolean isCheckingOut() {
        return !reserved && checkedOut;
    }

    /**
     * 是否被指定的读者借出中
     *
     * @param readerId 读者Id
     * @return 是否被指定的读者借出中
     */
    private boolean isCheckingOutBy(Long readerId) {
        return this.isCheckingOut() && readerId.equals(this.checkedOutBy);
    }

    /**
     * 借出超时是否已被标记
     *
     * @return 借出超时是否已被标记
     */
    private boolean isCheckedOutOverdueMarked() {
        return this.checkedOutOverdueMarked != null && this.checkedOutOverdueMarked;
    }
}
